<?php

/**
 * Picora™ : PHP Micro Framework
 * http://livepipe.net/projects/picora/
 * 
 * Copyright (c) 2007 LivePipe LLC
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to
 * deal in the Software without restriction, including without limitation the
 * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
 * sell copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 * DEALINGS IN THE SOFTWARE.
 * 
 * @author Ryan Johnson <ryan@livepipe.net>
 * @copyright 2007 LivePipe LLC
 * @package Picora
 * @version 1.0 Beta 2
 * @license MIT
 */

if(!function_exists('__autoload')){
	/**
	 * Defines the hook for the AutoLoader class to run.
	 */
	function __autoload($class_name){
		return PicoraAutoLoader::load($class_name);
	}
}

/**
 * Provides a nicer print out of the stack trace when an exception is thrown. You can use this funciton in your own custom exception handler by using the return parameter.
 *
 * This function is automatically set as the default exception handler. If you set one before including Picora in your application, just call restore_exception_handler() right after including it.
 * @param Exception $e Exception object.
 * @param boolean $return Return the stack trace as a string, or print it out.
 */
function picora_exception_handler($e,$return = false){
	$s = '<style>ul,li,pre {font-family:\'Lucida Grande\',Verdana; color:#333;} pre {margin-left:25px;}</style>';
	$s .= '<p style=" font-family:\'Lucida Grande\',Verdana; color:#911; font-weight:bold; font-size:12px; line-height:28px; padding:0; margin:0 0 5px 10px; float:none; border:none; background-image:none;">Uncaught '.get_class($e).'</p>';
	if(method_exists($e,'getTitle')){
		$s .= '<h1 style="color:#333; font-family:Verdana; font-size:24px; font-weight:bold; background-image:none; border:none; float:none; padding:0; margin:10px 0 15px 10px;">'.$e->getTitle().'</h1>';
		$s .= '<p style=" font-family:\'Lucida Grande\',Verdana; color:#333; font-size:16px; line-height:28px; padding:0; margin:0 0 15px 10px; float:none; border:none; background-image:none;">'.$e->getMessage().'</p>';
	}else{
		if(!is_object($e)){
			ob_start(); var_dump($e); $r = ob_get_clean();
			die('Unknown Error: Exception handler was not passed an Exception object. Was passed : '.$r.'<br/>Stack Trace:<br/><pre>'.print_r(debug_backtrace(),true).'</pre>');
		}
		$s .= '<h1 style="color:#333; font-family:Verdana; font-size:24px; font-weight:bold; background-image:none; border:none; float:none; padding:0; margin:10px 0 15px 10px;">'.$e->getMessage().'</h1>';
	}
	$max = 64;
	$traceArr = $e->getTrace();
	if(sizeof($traceArr) == 1)
		$s.= '<p style="font-family:\'Lucida Grande\',Verdana; font-size:12px; color:#444; text-align:left; line-height:24px; margin:0 0 0 10px; padding:0;"><b>The exception was thrown on line '.$e->getLine().' in '.$e->getFile().'</b></p>';
	else {
		//printout code from php.net comments on debug_backtrace()
		$s .= '<p style="font-family:\'Lucida Grande\',Verdana; font-size:12px; color:#444; text-align:left; line-height:24px; margin:0 0 0 10px; padding:0;"><b>Before the Exception was thrown, the script called the following functions in this order:</b><br>';
		$totalTabs = sizeof($traceArr) - 1;
		$usedTabs = 0;
		foreach(array_reverse($traceArr) as $arr){
			++$usedTabs;
			$s.= '&nbsp;&nbsp;&nbsp;';
			if (isset($arr['class'])) $s .= '<b>'.$arr['class'].'</b>&rarr;';
			$args = array();
			if(!empty($arr['args'])) foreach($arr['args'] as $v){
				if (is_null($v)) $args[] = 'null';
				else if (is_array($v)) $args[] = 'Array['.sizeof($v).']';
				else if (is_object($v)) $args[] = get_class($v).' Object';
				else if (is_bool($v)) $args[] = $v ? 'true' : 'false';
				else if (is_int($v)) $args[] = $v;
				else{
					$v = (string) @$v;
					$str = htmlspecialchars(substr($v,0,$max));
					if (strlen($v) > $max) $str .= '...';
					$args[] = "\"".$str."\"";
				}
			}
			$s .= '<b>'.$arr['function'].'(</b>'.implode(', ',$args).'<b>)</b>';
			$Line = (isset($arr['line'])? $arr['line'] : "unknown");
			$File = (isset($arr['file'])? $arr['file'] : "unknown");
			$s .= sprintf("&nbsp;&nbsp;&nbsp;<span style=\"font-size:18px;\">&raquo;</span>&nbsp;&nbsp;called on line %d in %s",$Line,$File).'<br/>';
			$s .= str_repeat('&nbsp;&nbsp;&nbsp;',$usedTabs);
		}
		$s .= '&nbsp;&nbsp;&nbsp;<b>The exception was thrown on line '.$e->getLine().' in '.$e->getFile().'</b>';
		$s .= '</p>';
	}
	if($return)
		return $s;
	else
		print $s;
}
set_exception_handler('picora_exception_handler');

/**
 * The AutoLoader class is an object oriented hook into PHP's __autoload functionality. You can add
 * 
 * - Single Files PicoraAutoLoader::addFile('PageController','controllers/PageController.php');
 * - Multiple Files PicoraAutoLoader::addFile(array('class'=>'file','class'=>'file'));
 * - Whole Folders PicoraAutoLoader::addFolder('path');
 *
 * When adding a whole folder each file should contain one class named the same as the file sans ".php" (PageController => PageController.php)
 *
 * __autoload is defined in the functions.php file.
 */
class PicoraAutoLoader {
	static protected $files = array();
	static protected $folders = array();
	
	/**
	 * PicoraAutoLoader::addFile('Controller','/path/to/Controller.php');
	 * PicoraAutoLoader::addFile(array('Controller'=>'/path/to/Controller.php','View'=>'/path/to/View.php'));
	 * @param mixed $class_name string class name, or array of class name => file path pairs.
	 * @param mixed $file Full path to the file that contains $class_name.
	 */
	static public function addFile($class_name,$file = false){
		if(!$file && is_array($class_name))
			foreach($class_name as $key => $value)
				self::addFile($key,$value);
		else
			self::$files[$class_name] = $file;
	}
	
	/**
	 * PicoraAutoLoader::addFolder('/path/to/my_classes/');
	 * PicoraAutoLoader::addFolder(array('/path/to/my_classes/','/more_classes/over/here/'));
	 * @param mixed $folder string, full path to a folder containing class files, or array of paths.
	 */
	static public function addFolder($folder){
		if(is_array($folder))
			foreach($folder as $f)
				self::addFolder($f);
		else
			self::$folders[] = $folder;
	}
	
	static public function load($class_name){
		foreach(self::$files as $name => $file){
			if($class_name == $name){
				require_once($file);
				return true;
			}
		}
		foreach(self::$folders as $folder){
			if(substr(0,-1) != DIRECTORY_SEPARATOR)
				$folder .= DIRECTORY_SEPARATOR;
			if(file_exists($folder.$class_name.'.php')){
				require_once($folder.$class_name.'.php');
				return true;
			}
		}
		return false;
	}
}

/**
 * The PicoraController class should be the parent class of all of your PicoraController sub classes that contain the business logic of your application (render a blog post, log a user in, delete something and redirect, etc).
 *
 * In the Dispatcher class you can define what urls / routes map to what Controllers and methods. Each method can either:
 *
 * - return a string response
 * - redirect to another method
 *
 * If the method returns null or false, the PicoraDispatcher will keep looking for another method that returns a response or redirects, calling the error callback if no method responds to the requested URL.
 *
 * Each controller can have a beforeCall() and afterCall() method which will be called before a method inside the Controller is called (even if no method returns a valid response), and after a method inside the PicoraController is called (only if the method returns a valid response).
 * 
 * When render() is used, the variable $controller containing the current Controller object is available as a local variable inside the View object being rendered.
 * 
 * Additionally, you can flash() variables that will appear as local variables if a View object is rendered on the next request.
 */
abstract class PicoraController {
	//used internall by Dispatcher to create a new PicoraController instance and call the requested method
	static public function call($class_and_method,$parameters,$arguments = false){
		$arguments = ($arguments) ? $arguments : array();
		$instance = new $class_and_method[0];
		$instance->params = $parameters;
		$instance->beforeCall($class_and_method[1],$arguments);
		$response = call_user_func_array(array($instance,$class_and_method[1]),$arguments);
		if($response){
			$callback_response = $instance->afterCall($class_and_method[1],$arguments,$response);
			if(!is_null($callback_response))
				$response = $callback_response;
		}
		return $response;
	}
	
	/**
	 * This is a callback function that will be called each time any method of the controller is called. This method may be called multiple times while the dispatcher is searching for a method that returns a response. It is designed to be overriden by a subclass and does nothing by default. 
	 * @param string $method_name
	 * @param array $arguments Arguments that will be passed to $method_name.
	 */
	protected function beforeCall($method_name,$arguments){}
	
	/**
	 * After a method sucessfully responds to a requested url, this method is called. It is designed to be overriden by a subclass and does nothing by default.
	 * @param string $method_name
	 * @param array $arguments Arguments that were passed to $method_name.
	 * @param string $response The response that was returned from $method_name.
	 * @return mixed Method should return null to leave $response untouched, otherwise the return value from the method becomes the response.
	 */
	protected function afterCall($method_name,$arguments,$response){}
	
	/**
	 * $this->render('views/blog/post.php',array('post'=>$post));
	 * @param string $file Path to the file to render. Path will be relative to the file that handles the request.
	 * @param mixed $local_variables Key => value pairs to pass to the View object that is rendered.
	 */
	public function render($file,$local_variables = false){
		return new PicoraView($this,$file,$local_variables);
	}
	
	/**
	 * Renders $data_to_encode as JSON with the appropriate headers, outputs this to the browser and terminates the current request.
	 * @param mixed $data_to_encode
	 */
	protected function renderJSON($data_to_encode,$use_standard = false){
		if(function_exists('json_encode'))
			$output = json_encode($data_to_encode);
		elseif(class_exists('JSON'))
			$output = JSON::encode($data_to_encode);
		else
			throw new Exception('No function or class found to render JSON data.');
		if($use_standard){
			header('X-JSON: ('.$output.')');
			print ' ';
		}else
			print '('.$output.')';
		exit;
	}
	
	/**
	 * Redirects to another method, and terminates the current request.
	 *
	 * Putting the word "Controller" at the end of each controller name is optional.
	 *
	 * $this->redirect('about');
	 * $this->redirect(array('BlogController','post'),array('post_id'=>5));
	 * $this->redirect(array('Blog','post'),array('post_id'=>5));
	 * @param mixed $controller_and_method String method name if redirecting to a method in the current controller or array('ControllerName','methodName') if redirecting to a method in another controller.
	 * @param mixed $arguments Array arguments to resolve the route,or boolean false.
	 */
	protected function redirect($controller_and_method,$arguments = false){
		header('Location: '.PicoraDispatcher::getUrl($controller_and_method,$arguments));
		exit;
	}
	
	/**
	 * Serializes $value and makes it available as a local variable with $key name on the next request in the rendered view.
	 * $this->flash('message','Your post has been saved.');
	 * $this->redirect('post',array('post_id'=>$post_id));
	 * //in the view file that the method post renders... 
	 * <?php if(isset($message)):?><p class="message"><?php print $message;?></p><?php endif;?>
	 * @param string $key
	 * @param mixed $value Can be any data type or object that is serializable.
	 * @param boolean $now Flash the value during the current request only? Defaults to false (so it will be available on this request, and the next).
	 */
	protected function flash($key,$value,$now = false){
		$_SESSION['__flash__']['values'][$key] = serialize($value);
		$_SESSION['__flash__']['gc'][$key] = ($now ? 0 : 1);
	}
	
	/**
	 * @return string "get","post" or "ajax", depending on a request type. An empty POST request will resolve as a GET request.
	 */
	protected function getRequestMethod(){
		return (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest')
			? 'ajax'
			: (count($_POST) === 0 ? 'get' : 'post')
		;
	}
	
	/**
	 * Alias for Dispatcher::getUrl();
	 *
	 * Putting the word "Controller" at the end of each controller name is optional.
	 *
	 * $controller->getUrl('post',array('post_id'=>5)); //outputs "/blog/5"
	 * $controller->getUrl(array('BlogController','post'),array('post_id'=>5)); //outputs "/blog/5"
	 * $controller->getUrl(array('Blog','post'),array('post_id'=>5)); //outputs "/blog/5"
	 * @param mixed $class_and_method String method_name if reffering to a method in the Controller class that is currently responding, or array('ControllerName,'methodName') if referring to a method in another Controller.
	 * @param mixed $arguments Optional arguments to resolve the url.
	 * @param bool $include_base_url
	 * @return mixed String url or boolean false if the url could not be resolved.
	 */
	public function getUrl($controller_and_method,$arguments = false){
		return PicoraDispatcher::getUrl($controller_and_method,$arguments);
	}
	
	/**
	 * Alias for Dispatcher::isCurrent();
	 * @param mixed $class_and_method String method_name if reffering to a method in the Controller class that is currently responding, or array('ControllerName,'methodName') if referring to a method in another Controller.
	 * @param mixed $arguments Optional arguments to resolve the url.
	 * @return boolean Wether or not the given class, method and arguments match the current dispatched ones.
	 */
	public function isCurrent($controller_and_method,$arguments = false){
		return PicoraDispatcher::isCurrent($controller_and_method,$arguments);
	}	
}

/**
 * The Dispatcher class is responsible for mapping urls / routes to Controller methods. Each route that has the same number of directory components as the current requested url is tried, and the first method that returns a response with a non false / non null value will be returned via the Dispatcher::dispatch() method. For example:
 *
 * A route string can be a literal url such as '/pages/about' or contain named variables '/blog/$post_id'. Since these route strings can contain "$", they must always be enclosed by single quotes. In use the variables in the route string are collected in the order they appear and are passed as the arguments to the corresponding controller method.
 * 
 * PicoraDispatcher::addRoute(array(
 * 	'/' => array('Page','index'),
 * 	'/about/' => array('Page','about'),
 * 	'/blog/$post_id' => array('Blog','post'),
 * 	'/blog/$post_id/comment/$comment_id/delete' => array('Blog','deleteComment')
 * ));
 *
 * Visiting /about/ would call PageController::about(),
 * visiting /blog/5 would call BlogController::post(5)
 * visiting /blog/5/comment/42/delete would call BlogController::post(5,42)
 *
 * To link to BlogController::deleteComment(5,42) we would call Dispatcher::getUrl(array('Blog','post'),array('post_id'=>5,'comment_id'=>42))
 *
 * The dispatcher is used by calling Dispatcher::addRoute() to setup the route(s), and Dispatcher::dispatch() to handle the current request and get a response.
 */
class PicoraDispatcher {
	//internally called by Dispatcher::dispatch()
	static protected function load(){
		@session_start();		
		if(!isset($_SESSION['__flash__']))
			$_SESSION['__flash__'] = array('values'=>array(),'gc'=>array());
		register_shutdown_function(array('PicoraDispatcher','flashGarbageCollection'));
	}
	
	//called on shutdown to clear out stale flash values
	static public function flashGarbageCollection(){
		foreach($_SESSION["__flash__"]["gc"] as $key => $value)
			--$_SESSION["__flash__"]["gc"][$key];
		foreach($_SESSION["__flash__"]["gc"] as $key => $value){
			if($value < 0){
				unset($_SESSION["__flash__"]["gc"][$key]);
				unset($_SESSION["__flash__"]["values"][$key]);
			}
		}
	}
	
	static protected $routes = array();
	static protected $status = array(
		'request_url' => '',
		'current_route' => '',
		'current_arguments' => array(),
		'current_controller' => '',
		'current_method' => '',
		'current_parameters' => array(),
		'flash_values' => array(),
		'dispatcher_dir' => '',
		'base_url' => ''
	);
	static protected $error_handler = array('PicoraDispatcher','error');
	
	/**
	 * Putting the word "Controller" at the end of each controller name is optional.
	 * Dispatcher::addRoute('/index',array('PageController','index'));
	 * Dispatcher::addRoute(array(
	 * 	'/blog/'=>array('Blog','index'),
	 * 	'/blog/:post_id'=>array('Blog','post')
	 * ));
	 * @param mixed $route String route or array of route => controller and method pairs.
	 * @param mixed $controller_and_method array('MyController','myMethod').
	 */
	static public function addRoute($route,$controller_and_method = false){
		if(!$controller_and_method && is_array($route))
			foreach($route as $key => $value)
				self::addRoute($key,$value);
		else{
			if(substr($controller_and_method[0],-10) != 'Controller')
				$controller_and_method[0] .= 'Controller';
			self::$routes[$route] = $controller_and_method;
		}
	}

	/**
	 * @param callback $callback The callback function that will be called (with the requested url as the only parameter) if no Controller responds to the requested URL.
	 */
	static public function setErrorHandler($callback){
		self::$error_handler = $callback;
	}
	
	/**
	 * Dispatcher::getRouteByClassAndMethod('BlogController','post'); //outputs: "/blog/:post_id"
	 * Dispatcher::getRouteByClassAndMethod(array('BlogController','post')); //outputs: "/blog/:post_id"
	 * @param mixed $class_name String class name or array(class_name,method_name).
	 * @param mixed $method_name String method name.
	 * @return mixed Returns the route string that matches the class name and method name
	 */
	static protected function getRouteByClassAndMethod($class_name,$method_name = false){
		if(!$method_name){
			$method_name = $class_name[1];
			$class_name = $class_name[0];
		}
		foreach(self::$routes as $route => $class_and_method)
			if($class_name == $class_and_method[0] && $method_name == $class_and_method[1])
				return $route;
		return false;
	}
	
	/**
	 * Putting the word "Controller" at the end of each controller name is optional.
	 *
	 * Dispatcher::getUrl('post',array('post_id'=>5)); //outputs "/blog/5"
	 * Dispatcher::getUrl(array('BlogController','post'),array('post_id'=>5)); //outputs "/blog/5"
	 * Dispatcher::getUrl(array('Blog','post'),array('post_id'=>5)); //outputs "/blog/5"
	 * @param mixed $class_and_method String method_name if reffering to a method in the Controller class that is currently responding, or array('ControllerName,'methodName') if referring to a method in another Controller.
	 * @param mixed $arguments Optional arguments to resolve the url.
	 * @param bool $include_base_url
	 * @return mixed String url or boolean false if the url could not be resolved.
	 */
	static public function getUrl($class_and_method,$arguments = false,$include_base_url = true){
		if(is_string($class_and_method))
			$class_and_method = array(self::$status['current_controller'].'Controller',$class_and_method);
		if(substr($class_and_method[0],-10) != 'Controller')
			$class_and_method[0] .= 'Controller';
		$route_string = self::getRouteByClassAndMethod($class_and_method[0],$class_and_method[1]);
		preg_match_all('/(?<!\\\\)(\$([^\/0-9][\w\_\-]*))/e',$route_string,$matches);
		foreach($matches[2] as $match){
			if($match == 'id' && isset($variables['id']) && $arguments['id'] === false)
				$route_string = str_replace('$id','new',$route_string);
			elseif(isset($arguments[$match]) && !is_null($arguments[$match]))
				$route_string = str_replace('$'.$match,$arguments[$match],$route_string);
			elseif(is_object($arguments) && method_exists($variables,'get'.str_replace(' ','',ucwords(str_replace('_',' ',$match)))))
				$route_string = str_replace('$'.$match,$arguments->{'get'.str_replace(' ','',ucwords(str_replace('_',' ',$match)))}(),$route_string);
		}
		return ($route_string && !preg_match('/\$[^\/]/',$route_string))
			? ($include_base_url ? substr(self::$status['base_url'],0,-1) : '').$route_string
			: false
		;
	}
	
	/**
	 * @param mixed $class_and_method String method_name if reffering to a method in the Controller class that is currently responding, or array('ControllerName,'methodName') if referring to a method in another Controller.
	 * @param mixed $arguments Optional arguments to resolve the url.
	 * @return boolean Wether or not the given class, method and arguments match the current dispatched ones.
	 */
	static public function isCurrent($class_and_method,$arguments = false){
		return (self::getUrl($class_and_method,$arguments,false) == self::$status['request_url']);
	}
	
	//used to call a controller and keep track of what class, method and route we are calling
	static protected function tryRoute($route,$class_and_method,$arguments = array()){
		self::$status['current_route'] = $route;
		self::$status['current_controller'] = substr($class_and_method[0],0,-10);
		self::$status['current_method'] = $class_and_method[1];
		self::$status['current_arguments'] = $arguments;
		return PicoraController::call($class_and_method,self::$status['current_parameters'],$arguments);
	}

	/**
	 * @param string $dispatcher_dir The directory that the application is running in.
	 * @param string $base_url The base url that the application is running at.
	 * @param string $requested_url The url that is being requested relative to the base url.
	 * @return string Returns the response from a Controller that responded to the requested url
	 */
	static public function dispatch($dispatcher_dir,$base_url,$requested_url){
		self::load();
		self::$status['current_parameters'] = array_merge($_POST,$_GET);
		unset(self::$status['current_parameters']['__route__']);
		self::$status['dispatcher_dir'] = $dispatcher_dir.'/';
		self::$status['base_url'] = $base_url;
		self::$status['request_url'] = $requested_url;
		self::$status['flash_values'] =& $_SESSION["__flash__"]["values"];
		foreach(self::$routes as $route => $class_and_method){
			if($requested_url == $route && ($response = self::tryRoute($route,$class_and_method)))
				return $response;
			if(preg_replace('{([^/]+)}','*',$route) == preg_replace('{([^/]+)}','*',$requested_url)){
				preg_match_all('{([^/]+)?}',$route,$route_components);
				preg_match_all('{([^/]+)?}',$requested_url,$requested_url_components);
				$arguments = array();
				foreach($requested_url_components[0] as $key => $requested_url_component){
					if($requested_url_component == '')
						continue;
					elseif(strpos($route_components[0][$key],'$') !== false)
						$arguments[] = $requested_url_component;
					elseif($route_components[0][$key] != $requested_url_component)
						continue(2);
				}
				if($response = self::tryRoute($route,$class_and_method,$arguments))
					return $response;
			}
		}
		return call_user_func_array(self::$error_handler,array($requested_url));
	}
		
	/**
	 * Key name can be any of the following:
	 * - 'dispatcher_dir' string Path to the directory where the file that handled the request is located. For example: "/Library/WebServer/Documents/my_app/"
	 * - 'base_url' string The base URL that the application is located at. For example: "http://localhost/my_app/"
	 * - 'request_url' string The URL that was requested relative to the base URL. For example: "/blog/5"
	 * - 'current_route' string The route string that matched the requested url. For example: "/blog/:post_id"
	 * - 'current_controller' string The name of the Controller that responded to the requested URL. For example: "Blog". same as current_class sans the word "Controller"
	 * - 'current_method' string The name of the method that responded to the requested URL. For example: "post"
	 * - 'current_parameters' array Merged POST and GET arrays.
	 * - 'current_arguments' mixed Array arguments passed to the called method, or bool false.
	 * @param mixed $key Boolean false or string key name.
	 * @return mixed If $key is specified, the value of the key will be returned, else array all key => value pairs.
	 */	
	static public function getStatus($key = false){
		return (!$key)
			? self::$status
			: (isset(self::$status[$key]) ? self::$status[$key] : false)
		;
	}
	
	//default error callback
	static protected function error($route_string){
		return 'Dispatch Error';
	}	
}

/**
 * The template object takes a valid path to a template file as the only argument in the constructor. You can then assign properties to the template, which become available as local variables in the template file. You can then call display() to get the output of the template, or just call print on the template directly thanks to PHP 5's __toString magic method.
 * 
 * $v = new PicoraView('views/my_template.php');
 * $v->title = 'My Title';
 * $v->body = 'My body';
 * print $v;
 * //or
 * print $v->display();
 * 
 * or the same thing in one command
 * 
 * print new PicoraView('views/my_template.php',array(
 * 	'title' => 'My Title',
 * 	'body' => 'My body'
 * ));
 * 
 * my_template.php might look like this: 
 * 
 * <html>
 * 	<head>
 * 		<title><?php print $title;?></title>
 * 	</head>
 * 	<body>
 * 		<h1><?php print $title;?></h1>
 * 		<p><?php print $body;?></p>
 * 	</body>
 * </html>
 * 
 * Using view helpers:
 * 
 * $v->addHelperMethod('tag',array('MyHelperClass','myTagGenerator'));
 * 
 * In your template you can now call: 
 *
 * <?php print $this->tag('a',array('href'=>'http://mysite.com/'),'My Link Text');?>
 * 
 * Sometimes it is useful to set content to be available outside of the current template.
 * 
 * <?php $this->beginContentFor('head');?>
 * 	This content will be available to all subsequent PicoraView objects in the variable $head.
 * <?php $this->endContentFor('head');?>
 *
 * This is useful in implementing layouts, so that templates may add to different sections. Your layout View must be rendered after the view declaring these sections is rendered for the variables to be available. The variables will be overwritten by any manually defined variables.
 */
class PicoraView {
	protected $__file__ = false;
	protected $__methods__ = array();
	protected $__controller__ = false;
	static protected $__content_blocks__ = array();
	
	/**
	 * @param Controller $c Controller object that is rendering the view.
	 * @param file_name $file Path to template file.
	 * @param mixed $params Optional array of key value pairs.
	 * @return View object
	 */
	final public function __construct(PicoraController $c,$file,$params = false){
		$this->__controller__ = $c;
		$this->__file__ = $file;
		if($params)
			foreach($params as $key => $value)
				$this->{$key} = $value;
	}
	
	/**
	 * Renders the current template, returning the result as a string.
	 * @return string
	 */
	public function display(){		
		//extract and unserialize all flash values and extract content blocks
		if(isset($_SESSION['__flash__']))
			foreach($_SESSION['__flash__']['values'] as $__key__ => $__value__)
				$$__key__ = unserialize($__value__);
		foreach(self::$__content_blocks__ as $__key__ => $__value__)
			$$__key__ =& $__value__;
		unset($__key__,$__value__);
		
		//bring public properties and $controller into scope
		extract(get_object_vars($this),EXTR_REFS);
		$controller = $this->__controller__;
		
		//include file and return output
		ob_start();
			include($this->__file__);
		return ob_get_clean();
	}
	
	final public function __toString(){
		return $this->display();
	}
	
	/**
	 * @param mixed String method name, or array of method name => callback pairs
	 * @param callback Callback function
	 */
	final public function addHelperMethod($method,$callback = false){
		if(is_array($method))
			foreach($method as $_method => $_callback)
				$this->addMethod($_method,$_callback);
		else
			$this->__methods__[$method] = $callback;
	}
	
	final public function __call($method,$args){
		if(isset($this->__methods__[$method]))
			return call_user_func_array($this->__methods__[$method],$args);
		else
			throw new Exception('Method '.get_class($this).'::'.$method.' not found.');
	}
	
	/**
	 * @param string Section name.
	 */
	final public function beginContentFor($section_name){
		ob_start();
	}
	
	/**
	 * @param string Section name.
	 */
	final public function endContentFor($section_name){
		if(isset(self::$__content_blocks__[$section_name]))
			self::$__content_blocks__[$section_name] .= ob_get_clean();
		else
			self::$__content_blocks__[$section_name] = ob_get_clean();
	}
}


?>